【小ネタ】Unreal Engine 4.22.3 + UnrealCVでsegmentation maskデータを取る方法。【備忘録】
せーのでございます。
秋に近づいてきますと、誰しも「ああ、segmentationのマスクデータが欲しいな。」、と感じるかと思います。
今日はそんなマスクデータをUE4からUnrealCVで取得しよう、というお話です。
経緯
UnrealCVというのはUnreal Engineのプラグインで、コンピュータビジョンのためのデータの取得や設定をコマンドからUEの世界に向けて入れられるようにするものです。
Unreal Engine 4で作ったバーチャル空間上で3Dモデルがぬるぬるアニメーションしているところを複数視点でcubemosの骨格検知にかけてみた
こんな感じで機械学習の推論などを仮想世界であるUnreal Engine上に展開できたりします。
逆にUnrealCVを使ってUnreal Engine(以下UE)上に配置されているモノや人などの各種データを取得することで、機械学習に使う教師データなどを簡単に作れます。
例えばobject_maskというコマンドを使うと、UEに置いてあるモノや人をマスクしたデータを取得できます。
こんなシーンから
こういう感じですね。
ただこのUnrealCV、リリースノートを見るとUE4.16までにしか対応していません。現在UEは4.25.3が最新です。最新とは言わないまでも、なるべく新しいバージョンに対応してほしい、というのが人情です。
そこで現在の開発を見ると、一番進んでいる開発ブランチは4.22であることがわかりました。
さっそくプラグインをビルドしてUE4.22.3に入れてみると、、、何も映らない。
問い合わせてみると、少し工夫が必要だそうなので、備忘録としてブログを書いておきます。
やり方
プラグインをビルド
まずはgithubから4.22ブランチのソースをチェックアウト(もしくはクローン)し、build用のスクリプトを叩きます。
F:\unrealcv>python build.py Found UE4 in the following path, please make a selection: 1 : C:\Program Files\Epic Games\UE_4.20 2 : C:\Program Files\Epic Games\UE_4.22 2 Running AutomationTool... Parsing command line: BuildPlugin -plugin=F:\unrealcv\UnrealCV.uplugin -package=F:\unrealcv\Plugins\UnrealCV -rocket -targetplatforms=Win64 Copying 233 file(s) using max 64 thread(s) Reading plugin from F:\unrealcv\Plugins\UnrealCV\HostProject\Plugins\UnrealCV\UnrealCV.uplugin... Building plugin for host platforms: Win64 ...... ...... ...... [63/73] SensorBPLib.gen.cpp [64/73] PlayerViewMode.gen.cpp [65/73] PawnCamSensor.gen.cpp [66/73] SerializeBPLib.gen.cpp [67/73] TcpServer.gen.cpp [68/73] UnrealCV.init.gen.cpp [69/73] ExecStatus.cpp [70/73] UnrealcvGameMode.gen.cpp [71/73] StereoCameraActor.gen.cpp [72/73] VisionBPLib.gen.cpp [73/73] ActorController.cpp Total time in Parallel executor: 148.19 seconds Total execution time: 157.15 seconds Took 157.235092s to run UnrealBuildTool.exe, ExitCode=0 Reading filter rules from F:\unrealcv\Plugins\UnrealCV\HostProject\Plugins\UnrealCV\Config\FilterPlugin.ini BUILD SUCCESSFUL AutomationTool exiting with ExitCode=0 (Success)
複数のバージョンが入っている場合はどのバージョン用のプラグインを作るか聞いてくる場合もありますので、その時は4.22のバージョンを選んでください。5分ほど待つとビルドが完了します。
出来上がると、ソースをダウンロードした場所に「Plugins」というフォルダができています。
F:\unrealcv>dir F:\unrealcv のディレクトリ 2020/10/14 13:08 <DIR> . 2020/10/14 13:08 <DIR> .. 2020/10/13 01:46 <DIR> .github 2020/10/14 13:07 691 .gitignore 2020/10/13 01:46 2,038 build.py 2020/10/13 01:46 <DIR> client 2020/10/13 01:46 <DIR> Config 2020/10/14 13:07 <DIR> Content 2020/10/14 13:07 <DIR> docs 2020/10/14 13:07 1,847 dodo.py 2020/10/13 01:46 <DIR> examples 2020/10/13 01:46 1,089 LICENSE 2020/10/14 13:08 <DIR> Plugins 2020/10/13 01:46 1,991 README.md 2020/10/13 01:46 <DIR> Resources 2020/10/13 01:46 <DIR> Source 2020/10/13 01:46 <DIR> test 2020/10/13 01:46 238 tox.ini 2020/10/14 13:07 716 UnrealCV.uplugin 7 個のファイル 8,610 バイト 12 個のディレクトリ 13,029,199,872 バイトの空き領域
プロジェクトにデプロイ
できた「Plugins」フォルダをUnrealCVが使いたいプロジェクトにまるっとコピーします。
プロジェクトを開いている場合は一旦閉じて、開きなおします。
[edit]=>[plugins]でプラグインパネルを開き、UnrealCVが入っていればOKです。
カメラを追加
デフォルトのカメラではデータが取れないので、「Fusion Camera Actor」というカメラを追加します。
画面左にある「Modes」というパネルからFusion Camera Actorというアクターを検索し、レベルにドラッグアンドドロップします。
追加したカメラの位置を調整します。私の場合はど真ん中に置いてみました。
デフォルトカメラを変更(やらなくてもよい)
このままPlayしてコマンドを叩いても動くのですが、画面上ではデフォルトカメラの映像が写っていますので、画角を確認するために追加したカメラからの映像を画面に映します。
画面上部の「Blueprints」から「Open level Blueprints」をクリックして、Blueprintsのパネルを開きます。
中央にある「Event Graph」エリアで右クリックして、Playした最初を表す「Event BeginPlay」、プレイヤー(つまり私)の入力を表す「Get Player Controller」をそれぞれ選択します。右クリックで出てくるアクションはものすごく多いのですが、出てくるパネルの上部に検索窓がついているので、それぞれ途中まで検索すると簡単に入れられます。
そして最後に「World Outliner」から先ほど入れた「FusionCameraActor1」(デフォルトではそういう名前になってます)をEvent Graphにドラッグアンドドロップします。World Outlinerは今ポップアップで出てくる前の画面、右側にあります。
それぞれパネルで管理されていてポップアップで新しいウィンドウがでてきたり、それをドラッグして元のウィンドウに結合させたりできるのはAdobe系のソフトのUIに似てますね。
さて、3つのノードがそろったら、これらをつなげていきます。まず、「Get Player Controller」の「Return Value」をマウスでつかんでEvent Graphの領域にドラッグアンドドロップすると、コントローラの入力値をどう返すかを選ぶポップアップが出てきますので、ここから「Set View Target with Blend」を選びます。
次にPlayを押した最初にこのBlueprintが動いてほしいので「Event BeginPlay」下の矢印を「Set View Target with Blend」まで伸ばします。
そして最後に「FusionCamerActor1」を「Set View Target with Blend」の「New View Target」までドラッグしてつなげます。
できたら画面中央上のコントロールパネルから「Compile」をクリックして完成です。出来たらこの画面は閉じても構いません。
play、コマンド入力
さて、いよいよコマンド入力です。先ほどの画面から中央上の「Play」ボタンをクリックすると、新しく追加したカメラからの画面が映ります。
この画面(ビューポート、と言います)をクリックすると、コントロールがUE内の世界に入ります。その状態で「`」(バッククォート)キーを押すとUnrealCVのコマンドが入力できます。一回押すと画面下部にコマンドラインが表示され、二回押すと画面上にコンソールとして表示されます。コンソールの場合、戻り値も表示されます。
試しに現在のステータスの状態を見てみます。ステータスを見るコマンドは
vget /unrealcv/status
です。
コマンドが通っていることを確認出来たら、マスクデータを取得していきましょう。
単に今見ている画面を変えたい場合は[vset /viewmode <変えたいモード>]、データとして取得したい場合は[vget /camera/
モードには
- lit: 通常のRGB画面
- depth: 深度データ(カメラからの距離を表す)
- normal: 法線マップ(カメラのある面に対しての法線ベクトルを色で表す。凹凸を表現するのに使う)
- object_mask: マスクデータ。今回の目的。
なんかがあります。
vset /viewmode depth
vset /viewmode normal
vget /camera/1/object_mask F:\objectmask.png(保存先のパス。任意)
うん、いい感じですね。
人だけ取ってマスクデータ化する
UEでは自分で好きなものやキャラクターを配置して動かしていますので、当然オブジェクトごとの識別もできます。それはつまり、必要なデータだけをマスクして取ってくる、ということも可能なわけです。機械学習の教師データを作るときには大事な要素ですね。
UnrealCVのチュートリアルを元に、Jupyter notebookで人だけをマスクしたデータを取得してみます。前提としてunrealcvモジュールをインストールしておきます。
pip install unrealcv
まず必要なライブラリと関数を定義しておきます。
%matplotlib inline import time; print(time.strftime("The last update of this file: %Y-%m-%d %H:%M:%S", time.gmtime())) from __future__ import division, absolute_import, print_function import os, sys, time, re, json import numpy as np import matplotlib.pyplot as plt imread = plt.imread def imread8(im_file): ''' Read image as a 8-bit numpy array ''' im = np.asarray(Image.open(im_file)) return im def read_png(res): import io from io import StringIO import PIL.Image img = PIL.Image.open(io.BytesIO(res)) return np.asarray(img) def read_npy(res): import io from io import StringIO return np.load(io.BytesIO(res))
次に動作中のUEに接続します。
from unrealcv import client client.connect() if not client.isconnected(): print('UnrealCV server is not running. Run the game downloaded from http://unrealcv.github.io first.') sys.exit(-1)
INFO:__init__:192:Got connection confirm: b'connected to MyProject4'
疎通確認のためステータス確認のコマンドを投げます。
res = client.request('vget /unrealcv/status') # The image resolution and port is configured in the config file. print(res)
Is Listening Client Connected 9000 Configuration Config file: C:/Program Files/Epic Games/UE_4.22/Engine/Binaries/Win64/unrealcv.ini Port: 9000 Width: 640 Height: 480 FOV: 90.000000 EnableInput: true EnableRightEye: false
人のオブジェクトのマスクした際の色を取得します。人のIDはUE4上の「World Outliner」で人のオブジェクトにマウスオーバーしても確認できますし
UnrealCVでは
vget /objects
でも一覧を取得できます。
UnrealCVにて
vget /object/<オブジェクトのID>/color
と入力するとマスクした色を取得できます。上の画像でわかるように今回人をマスクした時の色はピンクですので結果は
(R=255,G=127,B=255,A=255)
と返ってきます。
これを使って、該当する色以外はつぶしてしまうマスク関数を作って、必要なオブジェクトだけ残します。
scene_objects = client.request('vget /objects').split(' ') print('Number of objects in this scene:', len(scene_objects)) # TODO: replace this with a better implementation class Color(object): ''' A utility class to parse color value ''' regexp = re.compile('\(R=(.*),G=(.*),B=(.*),A=(.*)\)') def __init__(self, color_str): self.color_str = color_str #print(color_str) match = self.regexp.match(color_str) (self.R, self.G, self.B, self.A) = [int(match.group(i)) for i in range(1,5)] def __repr__(self): return self.color_str id2color = {} # Map from object id to the labeling color for obj_id in scene_objects: print("id: " + obj_id) obj = client.request('vget /object/%s/color' % obj_id) print(obj) if obj == "error Can not find object": scene_objects.remove(obj_id) print("remove id: " + obj_id) continue color = Color(obj) id2color[obj_id] = color print('%s : %s' % (obj_id, str(color))) scene_objects.pop() print (scene_objects)
def match_color(object_mask, target_color, tolerance=3): match_region = np.ones(object_mask.shape[0:2], dtype=bool) for c in range(3): # r,g,b min_val = target_color - tolerance max_val = target_color + tolerance channel_region = (object_mask[:,:,c] >= min_val) & (object_mask[:,:,c] <= max_val) match_region &= channel_region if match_region.sum() != 0: return match_region else: return None id2mask = {} for obj_id in scene_objects: color = id2color[obj_id] mask = match_color(object_mask, [color.R, color.G, color.B], tolerance = 3) if mask is not None: id2mask[obj_id] = mask
mask = id2mask['Petting_Animal_2'] plt.figure(); plt.imshow(mask)
キャラクターが女の子だけに若干怖くなってしまいましたが気にしません。
データを見てみる
ちなみにこのマスクをデータの形でprintすると
[False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False ... ... ... False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False False True True True True True True True True True True True True True True True True True True True True False False False True True True True False False False False False False False False False False False False False False False False False False False False False False False False False ... ... ...
とこんな感じでマスクしてあるところだけTrueになった全ピクセル分のデータになっています。
これと元画像("vget /camera/1/lit png" でとれます)をセットにしてぐるぐる回してあげれば、学習データがいい感じで取れそうですね。
まとめ
ということでUE4.22.3を使ったobject maskの取得方法を書きました。
そのうちUnrealCVも開発が進んでデフォルトの状態でもスパっととれるようになると思いますが、それまではこういう方法を使っていきましょう。